1 /**
2    Code to parse the output from `dub describe` and generate the main
3    test file automatically.
4  */
5 module unit_threaded.dub;
6 
7 import unit_threaded.from;
8 
9 struct DubPackage {
10     string name;
11     string path;
12     string mainSourceFile;
13     string targetFileName;
14     string[] flags;
15     string[] importPaths;
16     string[] stringImportPaths;
17     string[] files;
18     string targetType;
19     string[] versions;
20     string[] dependencies;
21     string[] libs;
22     bool active;
23 }
24 
25 struct DubInfo {
26     DubPackage[] packages;
27 }
28 
29 DubInfo getDubInfo(string jsonString) @trusted {
30     import std.json : parseJSON;
31     import std.algorithm : map, filter;
32     import std.array : array;
33 
34     auto json = parseJSON(jsonString);
35     auto packages = json.byKey("packages").array;
36     return DubInfo(packages.map!(a => DubPackage(a.byKey("name").str,
37             a.byKey("path").str, a.getOptional("mainSourceFile"),
38             a.getOptional("targetFileName"), a.byKey("dflags").jsonValueToStrings, a.byKey("importPaths")
39             .jsonValueToStrings, a.byKey("stringImportPaths").jsonValueToStrings, a.byKey("files")
40             .jsonValueToFiles, a.getOptional("targetType"), a.getOptionalList("versions"),
41             a.getOptionalList("dependencies"), a.getOptionalList("libs"),
42             a.byOptionalKey("active", true), //true for backwards compatibility
43             )).filter!(a => a.active).array);
44 }
45 
46 private string[] jsonValueToFiles(from!"std.json".JSONValue files) @trusted {
47     import std.algorithm : map, filter;
48     import std.array : array;
49 
50     return files.array.filter!(a => ("type" in a && a.byKey("type")
51             .str == "source") || ("role" in a && a.byKey("role").str == "source")
52             || ("type" !in a && "role" !in a)).map!(a => a.byKey("path").str).array;
53 }
54 
55 private string[] jsonValueToStrings(from!"std.json".JSONValue json) @trusted {
56     import std.algorithm : map, filter;
57     import std.array : array;
58 
59     return json.array.map!(a => a.str).array;
60 }
61 
62 private auto byKey(from!"std.json".JSONValue json, in string key) @trusted {
63     import std.json : JSONException;
64 
65     if (auto p = key in json.object)
66         return *p;
67     else
68         throw new JSONException("\"" ~ key ~ "\" not found");
69 }
70 
71 private auto byOptionalKey(from!"std.json".JSONValue json, in string key, bool def) {
72     if (auto p = key in json.object)
73         return (*p).boolean;
74     else
75         return def;
76 }
77 
78 //std.json has no conversion to bool
79 private bool boolean(from!"std.json".JSONValue json) @trusted {
80     import std.exception : enforce;
81     import std.json : JSONException, JSON_TYPE;
82 
83     enforce!JSONException(json.type == JSON_TYPE.TRUE
84             || json.type == JSON_TYPE.FALSE, "JSONValue is not a boolean");
85     return json.type == JSON_TYPE.TRUE;
86 }
87 
88 private string getOptional(from!"std.json".JSONValue json, in string key) @trusted {
89     if (auto p = key in json.object)
90         return p.str;
91     else
92         return "";
93 }
94 
95 private string[] getOptionalList(from!"std.json".JSONValue json, in string key) @trusted {
96     if (auto p = key in json.object)
97         return (*p).jsonValueToStrings;
98     else
99         return [];
100 }
101 
102 DubInfo getDubInfo(in bool verbose) {
103     import std.json : JSONException;
104     import std.conv : text;
105     import std.algorithm : joiner, map, copy;
106     import std.stdio : writeln;
107     import std.exception : enforce;
108     import std.process : pipeProcess, Redirect, wait;
109     import std.array : join, appender;
110 
111     if (verbose)
112         writeln("Running dub describe");
113 
114     immutable args = ["dub", "describe", "-c", "unittest"];
115     auto pipes = pipeProcess(args, Redirect.stdout | Redirect.stderr);
116     scope (exit)
117         wait(pipes.pid); // avoid zombies in all cases
118     string stdoutStr;
119     string stderrStr;
120     enum chunkSize = 4096;
121     pipes.stdout.byChunk(chunkSize).joiner.map!"cast(immutable char)a".copy(appender(&stdoutStr));
122     pipes.stderr.byChunk(chunkSize).joiner.map!"cast(immutable char)a".copy(appender(&stderrStr));
123     auto status = wait(pipes.pid);
124     auto allOutput = "stdout:\n" ~ stdoutStr ~ "\nstderr:\n" ~ stderrStr;
125 
126     enforce(status == 0, text("Could not execute ", args.join(" "), ":\n", allOutput));
127     try {
128         return getDubInfo(stdoutStr);
129     } catch (JSONException e) {
130         throw new Exception(text("Could not parse the output of dub describe:\n",
131                 allOutput, "\n", e.toString));
132     }
133 }
134 
135 bool isDubProject() {
136     import std.file;
137 
138     return "dub.sdl".exists || "dub.json".exists || "package.json".exists;
139 }
140 
141 // set import paths from dub information
142 void dubify(ref from!"unit_threaded.runtime".Options options) {
143 
144     import std.path : buildPath;
145     import std.algorithm : map, reduce;
146     import std.array : array;
147 
148     if (!isDubProject)
149         return;
150 
151     auto dubInfo = getDubInfo(options.verbose);
152     options.includes = dubInfo.packages.map!(
153             a => a.importPaths.map!(b => buildPath(a.path, b)).array).reduce!((a, b) => a ~ b)
154         .array;
155     options.files = dubInfo.packages[0].files;
156 }